#! /usr/bin/env python
# pymetapop.py

"""Module includes classes to represent objects necessary in describing 
metapopulation dynamics of a system.
"""

from types import *
from shapely.geometry import asShape
import pymetapop

class Patch(object):
    """Represents a patch entity in a population/metapopulation. __init__ 
    takes parameters as input or else an empty presentation is
    created.
    """

    # Keeps a record of instantiated patches
    patch_number = 0
    
    def __init__(self, id = None, coords = None, area = None, occupancy = None):
            self._id = id
            # Coords can is a sequence: either a list or a tuple
            self._coords = coords
            
            # If cooordinates are provided form a geometry
            if self._coords is not None:
                self.setgeometry()
            else:
                self._geometry = None
            
            # If provided coordinates are a polygon, get area
            # else use user provided (may be None)
            if self._geometry.type == 'Polygon':
                self._area = self._geometry.area
            else:
                self._area = area
            
            self._occupancy = occupancy
            self._nneighbour = None
            
            # A list holding the names for important parameters
            self._params = ['id', 'coords', 'geometry', 'area', 'occupancy',
                           'si', 'nneighbour']
            
            # Add to total number of patches
            Patch.patch_number = Patch.patch_number + 1
    
    @property
    def __geo__interface__(self):
        """Provides a simple interface to Shapely geometries
        """
        if len(self._coords) == 2:
            return {'type': 'Point', 'coordinates': self._coords}
        elif len(self._coords) == 1:
            return {'type': 'Polygon', 'coordinates': self._coords}
        else:
            return None
        
    def __str__(self):
        values = self.getparams()
        return '\n'.join((item + ':' + self.padding(item) *  '\t' + values[item]) 
                                           for item in self._params) 
    
    def __del__(self):
        Patch.patch_number = Patch.patch_number - 1
    
    def getid(self):
        return self._id

    def setid(self, value):
        assert type(value) is IntType, "id is not an integer: %s" % type(value)
        assert value > 0, "value for id is negative: %s" % value
        self._id = value
    
    def delid(self):
        del self._id
        
    def getcoords(self):
        return self._coords
    
    def setcoords(self, value):
        assert type(value) in (TupleType, ListType), "coords is not a sequence: %s" % type(value)
        assert len(value) >= 2, "coords has too few values: %s" % len(value)
        for i in value:
            assert type(i) is FloatType, "coordinate value is not a float: " \
                                          + str(i) + " is " + str(type(i))
            assert i > 0.0 , "coordinate value is negative: %s" % i
        self._coords = value
        self.setgeometry()
        
    def delcoords(self):
        del self._coords
        # If there is no coordinates there is no geometry
        del self._geometry
        
    def getgeometry(self):
        return self._geometry
    
    def setgeometry(self):
        assert self._coords is not None, "coordinates must be provided to form a geometry"
        self._geometry = asShape(self.__geo__interface__)
        
    def delgeometry(self):
        del self._geometry
        
    def getarea(self):
        return self._area   
    
    def setarea(self, value):
        assert type(value) in (FloatType, IntType), "area value is not a float or int: " + \
                                                    str(value) + " is " + str(type(value))
        assert value > 0, "value for area must be positive: %s" % value
        self._area = value
        
    def delarea(self):
        del self._area
        
    def getoccupancy(self):
        return self._occupancy
    
    def setoccupancy(self, value):
        """Occupancy is a boolean value 0/1 and must be true or false.
        """        
        assert type(value) is BooleanType, "occupancy must be boolean: %s" % type(value)
        self._occupancy = value         
        
    def deloccupancy(self):
        del self._occupancy
        
    def getnneighbour(self):
        """Method gets the ID of the nearest neighbour of the self.Patch
        
        return Patch."""
        if self._nneighbour is not None:
            return self._nneighbour.getid()
        else:
            return None
    
    def setnneighbour(self, patch):
        # TODO: class evaluation must be done in a better way
        assert patch.__class__ is pymetapop.Patch, "value must be Patch type: %s" % type(patch)
        self._nneighbour = patch
    
    def delnneighbour(self):
        del self._nneighbour
        
    def getparams(self):
        """Returns a dictionary with paramters and corresponding values. Keys 
        are parameter names held in class list params, values are corresponding
        values. All values are given as strings.
        """
        return {self._params[0]: str(self.getid()), self._params[1]: str(self.getcoords()), 
                self._params[2]: str(self.getgeometry()), self._params[3]: str(self.getarea()),
                self._params[4]: str(self.getoccupancy()), self._params[5]: str(self.getsi()),
                self._params[6]: str(self.getnneighbour())}
    
    def padding(self, string):
        """Helper functions that return a factor for tab padding in string
        represntation."""
        if len(string) < 7:
            return 2
        else:
            return 1
    
    id = property(getid, setid, delid, "id of the patch.")
    coords = property(getcoords, setcoords, delcoords, "coordinates of the patch.")
    geometry = property(getgeometry, setgeometry, delgeometry, "geometry of the patch.")
    area = property(getarea, setarea, delarea, "area of the patch.")
    occupancy = property(getoccupancy, setoccupancy, deloccupancy, "occupancy of the patch.")
    nneighbour = property(getnneighbour, setnneighbour, delnneighbour, "nearest neighbour of the patch")
    
class Network(object):
    """Represents a generic network of patches and provides different network
    level calculation methods.
    """

    def __init__(self):
        # Each network has a sequence of patches
        self.network = []

class Metapopulation(Network):
    """Represents a metapopulation network and provides additional 
    metapopulation dynamical methods.
    """

    def __init__(self):
        Network.__init__(self)
        self.network.append(1)

if __name__ == '__main__':
    point1 = (2.0, 2.5)
    point2 = (1.0, 0.5)
    polygon1 = (((1, 8), (2, 9), (3, 9), (3, 8)),)
    polygon2 = (((3, 7), (4, 6), (5, 5), (4, 4)),)
    x = pymetapop.Patch(1, polygon1)# ,2.3, 3.0])
    y = pymetapop.Patch(2, polygon2)# ,1.3, 0.9])
    x.setarea(13.4)
    y.setarea(2.3)
    x.setoccupancy(True)
    y.setoccupancy(False)
    x.setnneighbour(y)
    y.setnneighbour(x)
    print x.geometry.distance(y.geometry)
    print str(x)
    print
    print str(y)
    print
    
    met = Metapopulation()